fix(plugin-sharing): 记录删除后 sys_share_link 令牌立即失效 —— resolve 存在性校验 + 删除级联 (#5190) - #5214
Merged
Merged
Conversation
…除级联 (#5190) resolveToken 逐项检查 token/revoked_at/expires_at/audience/密码,唯独不问 「(object_name, record_id) 指向的记录还在不在」;sys_share_link 也没有任何 删除级联(#5103 的级联只覆盖 sys_record_share)。分享链接是无身份的能力令牌, 持有 URL 即拥有权限,所以这类孤儿比 #5103 更危险:记录 id 一旦被复用,早该 随记录消失的链接会直接对新记录生效。 两半同时落地,且第一半不依赖任何钩子跑过: 1. resolveToken 增加记录存在性检查,走与 revoked/expired 完全相同的分支返回 null(不区分、不另立错误码,避免把记录存在与否泄露给未授权持有者)。位置 在内存态检查之后、use_count/last_used_at 打点之前,因此死记录不再被计数; 探测抛错时 fail-closed(问不到 ≠ 放行)。 2. 记录删除级联到 sys_share_link,复用 #5103 已有的 seam:同一对全局 beforeDelete 行集暂存 + afterDelete 按 id 集合撤销、同一条串行 sweep 队列、 同一个 kernel:bootstrapped 孤儿清扫(keyset 分页、自报截断、每对象每页一次 批量存在性探测、探测失败一行不删)。两半互相隔离,撤销 grant 失败不会连带 跳过令牌。 链接一侧的姿态判定读 publicSharing(与 sharingModel 正交:最可能挂链接的对象 恰恰是记录共享谓词跳过的那类),且 publicSharing 声明过就算数——enabled 关掉 之前铸出的链接必须仍被清理。 机制本身抽到 record-orphan-cleanup.ts 由两张表共用,避免出现第二份必须与之 保持一致的 walk(chunk、上限、失败探测规则)。#5103 的既有行为逐字未变,其 305 条测试全绿即为证据。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
#5190 的链接级联用例全部跑在没有共享规则的对象上(contract / sys_report), 漏掉了唯一只在「携带最多共享机制」的对象上才存在的交互:一旦对象有规则, #5102 的 bindRuleHooks 会以另一个 hook package 注册自己的 beforeDelete / afterDelete,两个 package 读同一份 AFFECTED_ROWS_STASH_KEY 行集。只在自己是 唯一 beforeDelete 写入者时才成立的链接级联,能通过上面全部用例,却恰好在风险 最高的对象上继续泄漏令牌。 补三条:同一次删除里规则授权 + 手工共享 + 能力令牌三者一起消失;有界谓词删除 跨两个 hook package 仍只发一条集合式 sys_share_link 语句;两个 package 的绑定 互不干扰。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
…re-link-existence
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 10:20
os-zhuang
enabled auto-merge
August 4, 2026 10:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5190
ShareLinkService.resolveToken逐项检查 token、revoked_at、expires_at、audience、密码,唯独不问(object_name, record_id)指向的记录还在不在;sys_share_link也没有任何删除级联(#5103 的级联只覆盖sys_record_share)。于是链接在记录删除后照常 resolve,并继续 stampuse_count/last_used_at。这比 #5103 修的孤儿更危险,原因是结构性的:共享行点名了它的受益人,而分享链接是无身份的能力令牌——持有 URL 即拥有权限。一旦记录 id 被复用(自定义主键、保留原 id 的导入、未来的 id 回收),一个早该随记录消失的链接会直接对新记录生效,而持有者可以是当初被分享过的任意外部人员。
两半同时落地,且第一半不依赖任何钩子跑过
1.
resolveToken重新追问记录是否存在,并走与 revoked / expired 完全相同的分支返回null——不另立状态码、不另立错误,不让未授权持有者从端点里读出「那条记录被删了」这件事本身(否则每一个泄漏的令牌都成了该对象上的存在性 oracle)。探测位置在廉价的内存态门之后(revoked 的链接仍然不付任何查询代价)、在使用打点之前(死记录不再 stampuse_count/last_used_at)。探测抛错时 fail-closed:问不到 ≠ 放行。2. 记录删除级联到
sys_share_link,走 #5103 已有的 seam 而不是另起一套:同一对全局beforeDelete行集暂存 +afterDelete集合式撤销、同一条串行 sweep 队列、同一个kernel:bootstrapped孤儿清扫(keyset 分页、自报截断、每对象每页一次批量存在性探测、探测失败一行不删)。两半互相隔离——撤销 grant 时的驱动错误不会连带跳过令牌。链接一侧的姿态判定读
publicSharing,它与sharingModel正交:最可能挂链接的对象恰恰是记录共享谓词跳过的那类平台对象。publicSharing声明过就算数,即便当前enabled: false——之前铸出的链接必须仍被清理。孤儿链接行是删除而非 stamp
revoked_at:它的主体已经没了,不存在还需要留撤销记录的「链接」,而且这张表本来只增不减(Setup 的链接列表还会展示指向不存在记录的行)。管理员主动撤销的链接,其审计行原样保留。一致性方向
两条路径都拒绝在「问不到」的情况下行动,只是安全方向相反,这是刻意的:resolve 侧「问不到」必须拒绝授权,sweep 侧「问不到」必须一行不删(把瞬时驱动错误变成永久失访是不可接受的,没人能重新铸出一个别人已经持有的令牌)。
机制本身抽到
record-orphan-cleanup.ts由两张表共用(chunk、扫描上限、失败探测规则各一份),避免出现第二份必须与之保持一致的 walk。#5103 的既有行为逐字未变。验证
packages/spec/**与packages/objectql/**未改动一行;无 metadata / spec / API 形状变更。pnpm --filter @objectstack/plugin-sharing test→ 13 files / 347 tests passedpnpm --filter @objectstack/plugin-sharing typecheck→ cleaneslint --no-inline-config(8 个改动文件)→ exit 0check:durability-log-level→ ✓ 14 durability-critical catch seam(s), all loud or rethrowingresolveToken的存在性检查 → 5 failed(含 THE REPRO、不可区分性、不 stamp、fail-closed)links半边关掉 → 15 failed(有界/无界删除、boot sweep、有规则对象三条)已合入
origin/main(6 个提交,均不触及plugin-sharing)后复跑,仍全绿。🤖 Generated with Claude Code
https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
Generated by Claude Code